Micron Document
NexusPi Git Node

Commit 840f51da16c9ed9e051be8c1c8b3356da0a5c21f


Parents : 1cbed7a
Author : James L <jrl290@gmail.com>
Date : 2026-02-22T20:58:44-05:00

Add flash utility, display fixes, and comprehensive README

- flash.py: standalone flash utility with serial port listing, merge-bin,
GitHub Releases download, and esptool flash support
- Display.h: hide LAN row when Local TCP disabled, show local TCP port
instead of backbone port
- README.md: comprehensive documentation — Quick Start with 3 flash options,
OLED display layout, interface modes, routing customizations, path table
fix, interface name uniqueness, hardware rationale (PSRAM/flash)
- Release/boot_app0.bin: bundled for flash.py standalone use
- .gitignore: exclude merged firmware binary build artifact

Changes

5 files changed, 482 insertions(+), 23 deletions(-)

M Display.h +11 -13
M README.md +69 -10
A flash.py +401

Diff

diff --git a/.gitignore b/.gitignore
index afd39f3..a22554f 100755
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.hex
*.pyc
TODO
+rnodethv4_firmware.bin
Release/*.hex
Release/*.zip
Release/*.json

diff --git a/Display.h b/Display.h
index 747a463..b9c6395 100755
--- a/Display.h
+++ b/Display.h
@@ -840,17 +840,13 @@ void draw_stat_area() {
stat_area.print("wan");
}
- // Row 4 — LAN / local TCP server
- if (!boundary_state.ap_tcp_enabled) {
- stat_area.drawCircle(4, 33, 3, SSD1306_WHITE);
- stat_area.setCursor(10, 36);
- stat_area.print("LAN");
- } else if (boundary_state.ap_tcp_connected) {
- stat_area.fillCircle(4, 33, 3, SSD1306_WHITE);
- stat_area.setCursor(10, 36);
- stat_area.print("LAN");
- } else {
- stat_area.drawCircle(4, 33, 3, SSD1306_WHITE);
+ // Row 4 — LAN / local TCP server (hidden when disabled)
+ if (boundary_state.ap_tcp_enabled) {
+ if (boundary_state.ap_tcp_connected) {
+ stat_area.fillCircle(4, 33, 3, SSD1306_WHITE);
+ } else {
+ stat_area.drawCircle(4, 33, 3, SSD1306_WHITE);
+ }
stat_area.setCursor(10, 36);
stat_area.print("LAN");
}
@@ -971,9 +967,11 @@ void draw_disp_area() {
disp_area.print("No WiFi");
}
- // Backbone port
+ // Local TCP server port (shown only when enabled)
disp_area.setCursor(2, 55);
- disp_area.printf("Port:%u", boundary_state.tcp_port);
+ if (boundary_state.ap_tcp_enabled) {
+ disp_area.printf("Port:%u", boundary_state.ap_tcp_port);
+ }
// 1px separator after Port line
disp_area.drawLine(0, 60, disp_area.width()-1, 60, SSD1306_WHITE);

diff --git a/README.md b/README.md
index 5db4a75..c2d7574 100755
--- a/README.md
+++ b/README.md
@@ -31,6 +31,8 @@ Built on [microReticulum](https://github.com/attermann/microReticulum) (a C++ po
## Hardware
+The **Heltec WiFi LoRa 32 V4** was chosen because it ships standard with **2 MB PSRAM** and **16 MB flash** — enough headroom for the microReticulum transport tables, packet caching to flash storage, and the web-based configuration portal. Many other LoRa dev boards come with only 4–8 MB flash and no PSRAM, which would require significant compromises to the boundary node's caching and routing capabilities.
+
| Component | Spec |
|-----------|------|
| **Board** | Heltec WiFi LoRa 32 V4 |
@@ -41,28 +43,63 @@ Built on [microReticulum](https://github.com/attermann/microReticulum) (a C++ po
## Quick Start
-### Prerequisites
+### Option A: Easy Flash (no PlatformIO required)
+
+The easiest way to flash a pre-built firmware. You only need Python 3 and a USB cable.
+
+```bash
+# Install esptool (one time)
+pip install esptool
+
+# Clone this repo (or download just flash.py + the firmware binary)
+git clone https://github.com/jrl290/RNodeTHV4.git
+cd RNodeTHV4
+
+# Download latest firmware from GitHub Releases and flash
+python flash.py --download
+
+# Or flash a local binary
+python flash.py --file rnodethv4_firmware.bin
+```
+
+The flash utility will list all available serial ports and prompt you to choose one. If no ports are detected, you may need to hold the **BOOT** button while pressing **RESET** to enter download mode.
-- [PlatformIO](https://platformio.org/) installed (via VS Code extension or CLI)
-- Heltec WiFi LoRa 32 V4 connected via USB
+### Option B: Build from Source (PlatformIO)
-### Build & Flash
+For development or customization:
```bash
-# Clone this repo
+# Prerequisites: PlatformIO installed (VS Code extension or CLI)
+
git clone https://github.com/jrl290/RNodeTHV4.git
cd RNodeTHV4
# Build
pio run -e heltec_V4_boundary
-# Flash
+# Flash (via PlatformIO)
pio run -e heltec_V4_boundary -t upload
+# Or create a merged binary and flash with the utility
+python flash.py --merge-only # creates rnodethv4_firmware.bin
+python flash.py # flash it
+
# Monitor serial output (optional)
pio device monitor -e heltec_V4_boundary
```
+### Option C: Manual esptool Flash
+
+If you have the merged binary (`rnodethv4_firmware.bin`), you can flash it with a single esptool command:
+
+```bash
+esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 921600 \
+ write_flash -z --flash_mode qio --flash_freq 80m --flash_size 16MB \
+ 0x0 rnodethv4_firmware.bin
+```
+
+Replace `/dev/ttyACM0` with your serial port (`/dev/cu.usbmodem*` on macOS, `COM3` on Windows).
+
On first boot (or if no configuration is found), the device automatically enters the **Configuration Portal**.
## Configuration Portal
@@ -120,7 +157,7 @@ The 128×64 OLED is split into two panels:
● LORA ← filled circle = radio online
○ wifi ← unfilled circle = WiFi disconnected
● WAN ← filled = backbone TCP connected
- ○ LAN ← unfilled = no local TCP clients
+ ● LAN ← filled = local TCP client connected
────────────────
Air:0.3% ← current LoRa airtime
▓▓▓▓▓ ||||||| ← battery, signal quality
@@ -129,6 +166,7 @@ The 128×64 OLED is split into two panels:
- **Filled circle (●)** = active/connected
- **Unfilled circle (○)** = inactive/disconnected
- Labels are UPPERCASE when active, lowercase when inactive (except LAN which is always uppercase)
+- **LAN row is hidden** when the Local TCP Server is disabled in configuration — the remaining layout stays in place
### Right Panel — Device Info (64×64)
@@ -138,10 +176,13 @@ The 128×64 OLED is split into two panels:
SF7 125k ← spreading factor & bandwidth
──────────────── ← separator
192.168.1.42 ← WiFi IP address (or "No WiFi")
- Port:4242 ← backbone TCP port
+ Port:4242 ← Local TCP server port
──────────────── ← separator
```
+- **Port** shows the Local TCP server port (the port local nodes connect to), not the backbone port
+- **Port line is hidden** when the Local TCP Server is disabled
+
## Interface Modes
The firmware runs up to **three RNS interfaces** simultaneously, using different interface modes to control announce propagation and routing behavior:
@@ -164,6 +205,11 @@ The TCP backbone connection uses `MODE_BOUNDARY` (`0x20`), a custom implementati
If enabled, a TCP server on the WiFi network allows local Reticulum nodes to connect. It also uses Access Point mode, with the same announce filtering as LoRa.
+**Implementation details:**
+- Each TCP interface must have a **unique name** to produce a unique interface hash — the backbone uses `"TcpInterface"` and the local server uses `"LocalTcpInterface"`. Without distinct names, both interfaces produce the same hash, causing the interface map lookup to fail when routing packets.
+- TCP interfaces are configured with a **10 Mbps bitrate**, which causes Reticulum's Transport to prefer TCP paths over LoRa paths (typically ~1–10 kbps) when both are available for the same destination.
+- When the Local TCP Server is disabled, its status indicator (LAN) and port number are hidden from the OLED display.
+
## Routing & Memory Customizations
The ESP32-S3 has limited RAM compared to a desktop Reticulum node. Several customizations were made to the microReticulum library to operate reliably within these constraints:
@@ -217,6 +263,18 @@ This was changed to call `unpack()` instead, which parses all packet fields AND
> **Note:** `unpack()` only parses the plaintext routing envelope (destination hash, flags, hops, transport headers). It does not decrypt the end-to-end encrypted payload. Every Reticulum transport node performs equivalent header parsing during normal routing — this is standard behavior, not a security concern.
+### Path Table Update Fix
+
+The C++ `std::map::insert()` method silently does nothing when a key already exists — unlike Python's `dict[key] = value` which replaces. The original microReticulum code used `insert()` to update path table entries, meaning stale LoRa paths were never replaced by newer TCP paths (or vice versa).
+
+This was fixed by calling `erase()` before `insert()`, ensuring updated path entries always replace stale ones. Without this fix, the boundary node would continue routing packets via an old interface even after a better path was learned.
+
+### Interface Name Uniqueness
+
+Each RNS interface must have a **unique name** because the name is hashed to produce the interface identifier used in path table lookups. If two interfaces share the same name, they produce the same hash, and `std::map` can only store one — causing the Transport layer to fail to resolve the correct outbound interface for packets.
+
+The TcpInterface constructor accepts an explicit `name` parameter: the backbone uses `"TcpInterface"` and the local server uses `"LocalTcpInterface"`.
+
## Connecting to the Backbone
### Example: Connect to rmap.world
@@ -265,8 +323,9 @@ Set the boundary node's **Local TCP Server** to **Enabled** (port 4242).
| `RNode_Firmware.ino` | Main firmware — boundary mode initialization, interface setup, button handling |
| `BoundaryMode.h` | Boundary state struct, EEPROM load/save, configuration defaults |
| `BoundaryConfig.h` | Web-based captive portal for configuration |
-| `TcpInterface.h` | TCP backbone interface (implements `RNS::InterfaceImpl`) with HDLC framing |
+| `TcpInterface.h` | TCP interface for both backbone and local server (implements `RNS::InterfaceImpl`) with HDLC framing, unique naming, and 10 Mbps bitrate |
| `Display.h` | OLED display layout — boundary-specific status page |
+| `flash.py` | Flash utility — list serial ports, download from GitHub, merge & flash firmware |
| `Boards.h` | Board variant definition for `heltec32v4_boundary` |
| `platformio.ini` | Build targets: `heltec_V4_boundary` and `heltec_V4_boundary-local` |
@@ -276,7 +335,7 @@ The firmware depends on [microReticulum](https://github.com/attermann/microRetic
| File | Changes |
|------|---------|
-| `Transport.cpp` | Selective caching, default route forwarding, boundary-aware culling, `get_cached_packet()` unpack fix, memory limits |
+| `Transport.cpp` | Selective caching, default route forwarding, boundary-aware culling, `get_cached_packet()` unpack fix, path table `erase()+insert()` fix, memory limits |
| `Transport.h` | `MODE_BOUNDARY`, `PacketEntry`, `Callbacks`, `cull_path_table()`, configurable table sizes |
| `Identity.cpp` | `_known_destinations_maxsize` = 24, `cull_known_destinations()` |
| `Type.h` | `MODE_BOUNDARY` = 0x20, reduced `MAX_QUEUED_ANNOUNCES`, `MAX_RECEIPTS`, shorter timeouts |

diff --git a/Release/boot_app0.bin b/Release/boot_app0.bin
new file mode 100644
index 0000000..13562ca
Binary files /dev/null and b/Release/boot_app0.bin differ

diff --git a/flash.py b/flash.py
new file mode 100644
index 0000000..76cc1e9
--- /dev/null
+++ b/flash.py
@@ -0,0 +1,401 @@
+#!/usr/bin/env python3
+"""
+RNodeTHV4 Flash Utility
+
+Flash the RNodeTHV4 boundary node firmware to a Heltec WiFi LoRa 32 V4.
+No PlatformIO required — just Python 3 and a USB cable.
+
+Usage:
+ # Flash a pre-built merged binary (from GitHub Releases or local build)
+ python flash.py
+
+ # Flash a specific file
+ python flash.py --file rnodethv4_firmware.bin
+
+ # Download latest from GitHub and flash
+ python flash.py --download
+
+ # Specify serial port manually
+ python flash.py --port /dev/ttyACM0
+
+ # Just build the merged binary (requires PlatformIO build output)
+ python flash.py --merge-only
+"""
+
+import argparse
+import glob
+import hashlib
+import os
+import platform
+import shutil
+import subprocess
+import sys
+import time
+
+# ── Configuration ──────────────────────────────────────────────────────────────
+
+CHIP = "esp32s3"
+FLASH_MODE = "qio"
+FLASH_FREQ = "80m"
+FLASH_SIZE = "16MB"
+BAUD_RATE = "921600"
+MERGED_FILENAME = "rnodethv4_firmware.bin"
+GITHUB_REPO = "jrl290/RNodeTHV4"
+
+# Flash addresses for ESP32-S3 Arduino framework
+BOOTLOADER_ADDR = 0x0000
+PARTITIONS_ADDR = 0x8000
+BOOT_APP0_ADDR = 0xe000
+APP_ADDR = 0x10000
+
+# PlatformIO build output paths (relative to project root)
+BUILD_DIR = ".pio/build/heltec_V4_boundary"
+BOOTLOADER_BIN = os.path.join(BUILD_DIR, "bootloader.bin")
+PARTITIONS_BIN = os.path.join(BUILD_DIR, "partitions.bin")
+FIRMWARE_BIN = os.path.join(BUILD_DIR, "rnode_firmware_heltec32v4_boundary.bin")
+BOOT_APP0_BIN = os.path.expanduser(
+ "~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin"
+)
+
+# ── Helpers ────────────────────────────────────────────────────────────────────
+
+def find_esptool():
+ """Find esptool.py — bundled, pip-installed, or PlatformIO's copy."""
+ # 1. Bundled in Release/
+ bundled = os.path.join(os.path.dirname(__file__), "Release", "esptool", "esptool.py")
+ if os.path.isfile(bundled):
+ return [sys.executable, bundled]
+
+ # 2. pip-installed esptool
+ if shutil.which("esptool.py"):
+ return ["esptool.py"]
+ if shutil.which("esptool"):
+ return ["esptool"]
+
+ # 3. PlatformIO's esptool
+ pio_esptool = os.path.expanduser(
+ "~/.platformio/packages/tool-esptoolpy/esptool.py"
+ )
+ if os.path.isfile(pio_esptool):
+ return [sys.executable, pio_esptool]
+
+ return None
+
+
+def find_serial_port():
+ """List available serial ports and let the user choose."""
+ system = platform.system()
+
+ # Gather ports from glob patterns
+ if system == "Darwin":
+ patterns = ["/dev/cu.usbmodem*", "/dev/tty.usbmodem*",
+ "/dev/cu.usbserial*", "/dev/cu.SLAB*"]
+ elif system == "Linux":
+ patterns = ["/dev/ttyACM*", "/dev/ttyUSB*"]
+ else:
+ patterns = []
+
+ ports = []
+ for pattern in patterns:
+ ports.extend(glob.glob(pattern))
+
+ # Also try pyserial's port enumeration (works on all platforms including Windows)
+ try:
+ import serial.tools.list_ports
+ for port in serial.tools.list_ports.comports():
+ if port.device not in ports:
+ ports.append(port.device)
+ except ImportError:
+ pass
+
+ # Sort for consistent ordering
+ ports.sort()
+
+ if not ports:
+ return None
+
+ print("\nAvailable serial ports:")
+ for i, p in enumerate(ports):
+ print(f" [{i+1}] {p}")
+ print()
+
+ while True:
+ try:
+ choice = input(f"Select port [1-{len(ports)}]: ").strip()
+ idx = int(choice) - 1
+ if 0 <= idx < len(ports):
+ return ports[idx]
+ except (ValueError, EOFError):
+ pass
+ print("Invalid selection, try again.")
+
+
+def sha256_file(path):
+ """Compute SHA-256 hash of a file."""
+ h = hashlib.sha256()
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(65536), b""):
+ h.update(chunk)
+ return h.hexdigest()
+
+
+def download_firmware(dest_path):
+ """Download the latest merged firmware from GitHub Releases."""
+ try:
+ from urllib.request import urlretrieve, urlopen
+ import json
+ except ImportError:
+ print("Error: Python urllib not available.")
+ return False
+
+ api_url = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
+ print(f"Checking latest release from {GITHUB_REPO}...")
+
+ try:
+ with urlopen(api_url) as resp:
+ release = json.loads(resp.read())
+ except Exception as e:
+ print(f"Error fetching release info: {e}")
+ return False
+
+ # Find the merged firmware asset
+ asset_url = None
+ for asset in release.get("assets", []):
+ if asset["name"] == MERGED_FILENAME:
+ asset_url = asset["browser_download_url"]
+ break
+
+ if not asset_url:
+ print(f"Error: '{MERGED_FILENAME}' not found in latest release ({release.get('tag_name', '?')}).")
+ print("Available assets:", [a["name"] for a in release.get("assets", [])])
+ return False
+
+ print(f"Downloading {release['tag_name']} / {MERGED_FILENAME}...")
+ try:
+ urlretrieve(asset_url, dest_path)
+ except Exception as e:
+ print(f"Download failed: {e}")
+ return False
+
+ size = os.path.getsize(dest_path)
+ print(f"Downloaded {size:,} bytes SHA-256: {sha256_file(dest_path)[:16]}...")
+ return True
+
+
+def merge_firmware(output_path, esptool_cmd):
+ """Merge bootloader + partitions + boot_app0 + app into a single binary."""
+ # Check all required files exist
+ required = {
+ "bootloader": BOOTLOADER_BIN,
+ "partitions": PARTITIONS_BIN,
+ "firmware": FIRMWARE_BIN,
+ }
+
+ # boot_app0 can come from PlatformIO or be bundled
+ boot_app0 = BOOT_APP0_BIN
+ if not os.path.isfile(boot_app0):
+ # Check if bundled in Release/
+ alt = os.path.join(os.path.dirname(__file__), "Release", "boot_app0.bin")
+ if os.path.isfile(alt):
+ boot_app0 = alt
+ else:
+ print(f"Error: boot_app0.bin not found at {BOOT_APP0_BIN}")
+ print(" Run 'pio run -e heltec_V4_boundary' first, or install PlatformIO.")
+ return False
+ required["boot_app0"] = boot_app0
+
+ for name, path in required.items():
+ if not os.path.isfile(path):
+ print(f"Error: {name} not found: {path}")
+ print("Run 'pio run -e heltec_V4_boundary' to build first.")
+ return False
+
+ print("Merging firmware components...")
+ print(f" Bootloader: {BOOTLOADER_BIN} @ 0x{BOOTLOADER_ADDR:04x}")
+ print(f" Partitions: {PARTITIONS_BIN} @ 0x{PARTITIONS_ADDR:04x}")
+ print(f" boot_app0: {boot_app0} @ 0x{BOOT_APP0_ADDR:04x}")
+ print(f" Firmware: {FIRMWARE_BIN} @ 0x{APP_ADDR:05x}")
+
+ cmd = esptool_cmd + [
+ "--chip", CHIP,
+ "merge_bin",
+ "--flash_mode", FLASH_MODE,
+ "--flash_freq", FLASH_FREQ,
+ "--flash_size", FLASH_SIZE,
+ "-o", output_path,
+ f"0x{BOOTLOADER_ADDR:x}", BOOTLOADER_BIN,
+ f"0x{PARTITIONS_ADDR:x}", PARTITIONS_BIN,
+ f"0x{BOOT_APP0_ADDR:x}", boot_app0,
+ f"0x{APP_ADDR:x}", FIRMWARE_BIN,
+ ]
+
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ print(f"Error merging: {result.stderr}{result.stdout}")
+ return False
+
+ size = os.path.getsize(output_path)
+ print(f"\nMerged binary: {output_path} ({size:,} bytes)")
+ print(f"SHA-256: {sha256_file(output_path)[:16]}...")
+ return True
+
+
+def flash_firmware(firmware_path, port, esptool_cmd, baud=BAUD_RATE):
+ """Flash firmware to the device."""
+ print(f"\nFlashing {firmware_path} to {port}...")
+ print(f" Chip: {CHIP} Baud: {baud} Flash: {FLASH_SIZE}\n")
+
+ # Determine if this is a merged binary (flash at 0x0) or app-only (flash at 0x10000)
+ size = os.path.getsize(firmware_path)
+ if size > 1500000:
+ # Merged binary — includes bootloader, partitions, etc.
+ flash_addr = f"0x{BOOTLOADER_ADDR:x}"
+ else:
+ # App-only binary
+ flash_addr = f"0x{APP_ADDR:x}"
+
+ cmd = esptool_cmd + [
+ "--chip", CHIP,
+ "--port", port,
+ "--baud", baud,
+ "--before", "default_reset",
+ "--after", "hard_reset",
+ "write_flash",
+ "-z",
+ "--flash_mode", FLASH_MODE,
+ "--flash_freq", FLASH_FREQ,
+ "--flash_size", FLASH_SIZE,
+ flash_addr, firmware_path,
+ ]
+
+ print("Running: " + " ".join(cmd[-8:]))
+ result = subprocess.run(cmd)
+ return result.returncode == 0
+
+
+# ── Main ───────────────────────────────────────────────────────────────────────
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="RNodeTHV4 Flash Utility — flash boundary node firmware to Heltec V4",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ python flash.py # Flash local merged binary
+ python flash.py --download # Download latest release and flash
+ python flash.py --file firmware.bin # Flash a specific file
+ python flash.py --merge-only # Build merged binary from PlatformIO output
+ python flash.py --port /dev/ttyACM0 # Specify serial port
+ """,
+ )
+ parser.add_argument("--file", "-f", help="Path to firmware binary to flash")
+ parser.add_argument("--port", "-p", help="Serial port (auto-detected if omitted)")
+ parser.add_argument("--baud", "-b", default=BAUD_RATE, help=f"Baud rate (default: {BAUD_RATE})")
+ parser.add_argument("--download", "-d", action="store_true",
+ help="Download latest firmware from GitHub Releases")
+ parser.add_argument("--merge-only", action="store_true",
+ help="Merge PlatformIO build output into single binary, don't flash")
+ parser.add_argument("--no-merge", action="store_true",
+ help="Skip merge step, use existing merged binary or --file")
+
+ args = parser.parse_args()
+ baud = args.baud
+
+ print("╔══════════════════════════════════════════╗")
+ print("║ RNodeTHV4 Flash Utility ║")
+ print("║ Heltec WiFi LoRa 32 V4 Boundary Node ║")
+ print("╚══════════════════════════════════════════╝")
+ print()
+
+ # Find esptool
+ esptool_cmd = find_esptool()
+ if not esptool_cmd:
+ print("Error: esptool not found!")
+ print("Install it with: pip install esptool")
+ sys.exit(1)
+ print(f"Using esptool: {' '.join(esptool_cmd)}")
+
+ # Determine firmware file
+ firmware_path = None
+
+ if args.file:
+ firmware_path = args.file
+ if not os.path.isfile(firmware_path):
+ print(f"Error: file not found: {firmware_path}")
+ sys.exit(1)
+
+ elif args.download:
+ firmware_path = MERGED_FILENAME
+ if not download_firmware(firmware_path):
+ sys.exit(1)
+
+ elif args.merge_only:
+ if merge_firmware(MERGED_FILENAME, esptool_cmd):
+ print(f"\nDone! Flash with: python flash.py --file {MERGED_FILENAME}")
+ else:
+ sys.exit(1)
+ return
+
+ else:
+ # Try to find or create a merged binary
+ if os.path.isfile(MERGED_FILENAME) and not args.no_merge:
+ # Check if PlatformIO build is newer
+ if os.path.isfile(FIRMWARE_BIN):
+ build_time = os.path.getmtime(FIRMWARE_BIN)
+ merge_time = os.path.getmtime(MERGED_FILENAME)
+ if build_time > merge_time:
+ print("Build output is newer than merged binary, re-merging...")
+ if not merge_firmware(MERGED_FILENAME, esptool_cmd):
+ sys.exit(1)
+ firmware_path = MERGED_FILENAME
+ elif os.path.isfile(FIRMWARE_BIN):
+ # Build exists but no merged binary — create one
+ print("Found PlatformIO build output, creating merged binary...")
+ if not merge_firmware(MERGED_FILENAME, esptool_cmd):
+ sys.exit(1)
+ firmware_path = MERGED_FILENAME
+ elif os.path.isfile(MERGED_FILENAME):
+ firmware_path = MERGED_FILENAME
+ else:
+ print("No firmware found!")
+ print()
+ print("Options:")
+ print(" 1. Build with PlatformIO first: pio run -e heltec_V4_boundary")
+ print(" 2. Download from GitHub: python flash.py --download")
+ print(" 3. Specify a file: python flash.py --file <path>")
+ sys.exit(1)
+
+ # Flash
+ port = args.port or find_serial_port()
+ if not port:
+ print("\nError: No serial port detected!")
+ print("Connect your Heltec V4 via USB and try again,")
+ print("or specify manually: python flash.py --port /dev/ttyACM0")
+ sys.exit(1)
+
+ print(f"\nSerial port: {port}")
+ print(f"Firmware: {firmware_path} ({os.path.getsize(firmware_path):,} bytes)")
+ print()
+
+ confirm = input("Flash firmware? [Y/n] ").strip().lower()
+ if confirm and confirm != "y":
+ print("Aborted.")
+ sys.exit(0)
+
+ if flash_firmware(firmware_path, port, esptool_cmd, baud):
+ print()
+ print("╔══════════════════════════════════════════╗")
+ print("║ Flash complete! ║")
+ print("║ Device will reboot automatically. ║")
+ print("║ ║")
+ print("║ On first boot, hold PRG > 5s to enter ║")
+ print("║ the configuration portal. ║")
+ print("╚══════════════════════════════════════════╝")
+ else:
+ print("\nFlash FAILED. Check connection and try again.")
+ print("You may need to hold BOOT while pressing RESET.")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()

Served by rngit 1.4.2 - Generated in 0.07s